50. Sudoku al estilo de P. Norvig

Esta solución al Sudoku es una mínima re-escritura de la solución dada por Peter Norvig en su página.

Los cambios tan solo pretenden eliminar las decisiones sutiles, que no son propias de un aprendiz. Tal es el caso de la codificación de filas, columnas y valores como cadenas para evitar el uso de deepcopy.

Nosotros codificaremos las casillas con sus coordenadas cartesianas, como una simple tupla y los valores como conjuntos de números.

En primer lugar vamos a definir las estructuras que emplea Peter Norvig. Piénsalas tú mismo en casa, no las leas sin más. Escribe tú mismo una función que las calcule y emplea tantas funciones como necesites.

squares = [ (x,y) for y in range(9) for x in range(9) ]
blocks = [ (0,1,2), (3,4,5), (6,7,8) ]
unitlist = [ [ (x,y) for x in range(9) ] for y in range(9) ] + \
            [ [ (x,y) for y in range(9) ] for x in range(9) ] + \
            [ [ (x,y) for x in bx for y in by ] for bx in blocks for by in blocks ]
units = { s: [u for u in unitlist if s in u] for s in squares }
peers = { s: set(sum(units[s],[])) - { s } for s in squares }

Vamos ahora a definir funciones para convertir una cadena de texto a un Sudoku. El Sudoku lo modelaremos como un diccionario que hace corresponder a cada casilla con el conjunto de números que puede contener dicha casilla. Es prácticamente una trasliteración del código de Peter Norvig, por lo que no merece la pena comentarlo con mucho más detalle.

La función ignora cualquier carácter que no sea una cifra o un punto. El punto o el 0 representan casillas sin rellenar.

Inicialmente el Sudoku está vacío. Es decir, cada casilla puede contener cualquier cifra. Lo modelamos con la variable values que no es más que un diccionario que hace corresponder cada casilla al conjunto de valores permitidos en esa casilla. Usamos conjuntos (set) porque se esta forma garantizamos que no hay repetidos y además podemos eliminar elementos empleando el operador de resta.

Cada vez que conocemos una casilla con un dígito fijo (de 1 a 9) podemos asignar ese valor a la casilla eliminado el resto. Esta asignación hace que podamos simplificar otras casillas (propagación de restricciones).

digits = range(1,10)

def parse_grid(grid):
    values = { s:set(digits) for s in squares }
    for s,d in grid_values(grid).items():
        if d in digits:
            asignar(values, s, { d })
    return values

def grid_values(grid):
    values = [ gvalue(c) for c in grid if c in '.1234567890' ]
    assert len(values) == 81
    return dict(zip(squares, values))

def gvalue(c): return int(c) if c in '123456789' else 0

El punto clave de la solución del Sudoku es la propagación de restricciones. Básicamente consiste en la combinación de dos funciones que cooperan entre sí, una para asignar valores a una casilla y otra para eliminar valores de una casilla. Cuando se asigna un valor en una casilla, debe eliminarse de todos sus peers. Al eliminar valores de una casilla puede que queden nuevas casillas con un único valor, que daría lugar a nuevas eliminaciones. También puede que solo quede una casilla posible donde poner alguna de las cifras, por lo que daría lugar a la asignación de esta cifra.

def asignar(values, s, d):
    other_values = values[s] - d
    eliminar(values, s, other_values)

def eliminar(values, s, d):
    if not d.intersection(values[s]):
        return

    values[s] -= d
    if len(values[s]) == 0:
        raise ValueError('Casilla {0} sin valores posibles'.format(s))

    ## (1) Si una casilla se queda con un solo valor, eliminarlo de sus colegas.
    if len(values[s]) == 1:
        for s2 in peers[s]:
            eliminar(values, s2, values[s])

    ## (2) Si solo hay una casilla de una unidad para el digito d asignarlo a esa casilla.
    for u in units[s]:
        dplaces = [s for s in u if d.intersection(values[s])]
        if len(dplaces) == 0:
            raise ValueError('No hay casilla para valor {0} en unidad {1}'.format(d,u))
        elif len(dplaces) == 1:
            asignar(values, dplaces[0], d)

Va siendo hora de escribir una función para mostrar el Sudoku. Dibujaremos líneas para separar los bloques. Lo más destacable es el cálculo del ancho de cada casilla, que depende de cuántas cifras posibles tienen todas las casillas del Sudoku.

def display(values):
    width = 1+max(len(values[s]) for s in squares)
    line = '+'.join(['-'*(width*3)]*3)
    for r in range(9):
        print(''.join(''.join([str(i) for i in values[(c,r)]]).center(width)+('|' if c in (2,5) else '')
                      for c in range(9)))
        if r in (2,5): print(line)
    print()

Solo con esto ya se resuelven prácticamente todos los Sudokus fáciles.

grid1 = '003020600900305001001806400008102900700000008006708200002609500800203009005010300'
display(parse_grid(grid1))
4 8 3 |9 2 1 |6 5 7
9 6 7 |3 4 5 |8 2 1
2 5 1 |8 7 6 |4 9 3
------+------+------
5 4 8 |1 3 2 |9 7 6
7 2 9 |5 6 4 |1 3 8
1 3 6 |7 9 8 |2 4 5
------+------+------
3 7 2 |6 8 9 |5 1 4
8 1 4 |2 5 3 |7 6 9
6 9 5 |4 1 7 |3 8 2

En algunos casos la propagación de restricciones sigue dejando un Sudoku muy poco especificado.

grid2 = '4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......'
display(parse_grid(grid2))
   4      1679   12679  |  139     2369    1269  |   8      1239     5
 26789     3    1256789 | 14589   24569  1245689 | 12679    1249   124679
  2689   15689   125689 |   7     234569 1245689 | 12369   12349   123469
------------------------+------------------------+------------------------
  3789     2     135789 |  3459   34579    4579  | 13579     6     13789
  3679   15679   135679 |  359      8     25679  |   4     12359   12379
 36789     4     356789 |  359      1     25679  | 23579   23589   23789
------------------------+------------------------+------------------------
  289      89     289   |   6      459      3    |  1259     7     12489
   5      6789   36789  |   2      479    14789  |  1369   13489   134689
   1      6789     4    |  589     579     5789  | 23569   23589   23689

Aparentemente el número de posibilidades es aún muy elevado:

def nposibles(values):
    n = 1
    for s,v in values.items():
        n *= len(v)
    return n

print(nposibles(parse_grid(grid2)))
181432630923264000000000000000000000000000

Sin embargo si dirigimos adecuadamente la búsqueda el resultado puede ser muy rápido. Tomar una decisión sobre el valor de una casilla que tiene nueve posibilidades nos da un 11% de probabilidad de acierto. Sin embargo elegir una de las opciones de una casilla que solo tenga dos posibilidades nos da un 50% de probabilidad de acierto, y al propagar las restricciones se reduce la cardinalidad de las demás casillas, aumentando correspondientemente la probabilidad de acierto.

def solve(grid): return search(parse_grid(grid))

from copy import deepcopy

def search(values):
    if all(len(values[s]) == 1 for s in squares):
        return values

    ## Elige la casilla sin fijar con menos posibilidades
    n,s = min((len(values[s]), s) for s in squares if len(values[s]) > 1)
    return intentar_todos(values, s)

def intentar_todos(values, s):
    for d in values[s]:
        try:
            tentativa = deepcopy(values)
            asignar(tentativa, s, { d })
            return search(tentativa)
        except ValueError:
            pass

    raise ValueError('No hay solucion')
display(solve(grid2))
4 1 7 |3 6 9 |8 2 5
6 3 2 |1 5 8 |9 4 7
9 5 8 |7 2 4 |3 1 6
------+------+------
8 2 5 |4 3 7 |1 6 9
7 9 1 |5 8 6 |4 3 2
3 4 6 |9 1 2 |7 5 8
------+------+------
2 8 9 |6 4 3 |5 7 1
5 7 3 |2 9 1 |6 8 4
1 6 4 |8 7 5 |2 9 3
hard1  = '''
. . . |. . 6 |. . .
. 5 9 |. . . |. . 8
2 . . |. . 8 |. . .
------+------+------
. 4 5 |. . . |. . .
. . 3 |. . . |. . .
. . 6 |. . 3 |. 5 4
------+------+------
. . . |3 2 5 |. . 6
. . . |. . . |. . .
. . . |. . . |. . .'''
display(solve(hard1))
3 8 4 |5 1 6 |9 2 7
6 5 9 |4 7 2 |1 3 8
2 7 1 |9 3 8 |6 4 5
------+------+------
8 4 5 |2 6 9 |7 1 3
1 2 3 |7 5 4 |8 6 9
7 9 6 |1 8 3 |2 5 4
------+------+------
9 1 8 |3 2 5 |4 7 6
4 3 2 |6 9 7 |5 8 1
5 6 7 |8 4 1 |3 9 2
hard2 = '''
. . 5 |3 . . |. . .
8 . . |. . . |. 2 .
. 7 . |. 1 . |5 . .
------+------+------
4 . . |. . 5 |3 . .
. 1 . |. 7 . |. . 6
. . 3 |2 . . |. 8 .
------+------+------
. 6 . |5 . . |. . 9
. . 4 |. . . |. 3 .
. . . |. . 9 |7 . . '''
display(solve(hard2))
1 4 5 |3 2 7 |6 9 8
8 3 9 |6 5 4 |1 2 7
6 7 2 |9 1 8 |5 4 3
------+------+------
4 9 6 |1 8 5 |3 7 2
2 1 8 |4 7 3 |9 5 6
7 5 3 |2 9 6 |4 8 1
------+------+------
3 6 7 |5 4 2 |8 1 9
9 8 4 |7 6 1 |2 3 5
5 2 1 |8 3 9 |7 6 4
Next Section - 51. Sudoku